ADFA-2433 | random-xkcd: new demo plugin#6
Conversation
Adds the build, manifest skeleton, and `IPlugin` lifecycle class for a new Code on the Go plugin. The host loads this class via `DexClassLoader` by the fully-qualified name in `plugin.main_class`, then drives initialize → activate → deactivate → dispose. Wrap initialize() in try/catch so a stray exception in plugin setup doesn't crash the host IDE — returning false here makes the IDE skip activate() and keep running. Subsequent commits opt this class into UIExtension (bottom-sheet tab) and DocumentationExtension (in-IDE help). For now there are no extensions — the plugin loads cleanly but doesn't surface any UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugins declare permissions via comma-separated values in a single `plugin.permissions` meta-data entry — not Android's `<uses-permission>` system. The XKCD plugin needs exactly one: `network.access`, to fetch the xkcd JSON + image. CoGo's filesystem.* permissions gate access to the host IDE's project files, not the plugin's own `filesDir`/`cacheDir` — those are sandbox-allowed and need no declaration. The system clipboard also has no permission gate (no `clipboard.*` exists; Android itself doesn't gate clipboard either). So even with the triple-tap image- clipboard flow that comes in a later commit, this remains the only permission this plugin declares. The Tier-3 walkthrough that arrives later explains the conceptual model: permissions in CoGo gate the host's resources, not your own. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opts the plugin class into `UIExtension` and registers one tab in the editor bottom sheet alongside Build Output, App Logs, etc. The host's `getEditorTabs()` call returns a `TabItem` with a fragmentFactory the host invokes whenever the tab is shown. The Fragment is shell-only here: a layout inflated via `PluginFragmentHelper.getPluginInflater(pluginId, parent)` (required so `R.layout.*` resolves against the plugin's APK and not the host IDE's), view bindings, and an empty-state placeholder. Tap handling, network fetch, and clipboard arrive in subsequent commits. Includes the layout XML, string resources (with a CC BY-NC 2.5 attribution string for the xkcd credit shown beneath every comic), colors for day + night themes, and the plugin theme style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small purpose-built tap-burst state machine and wires it into the Fragment's touch listener. Android's `GestureDetector` resolves single + double taps but not triple, and the XKCD plugin needs all three (single = new comic, double = copy URL, triple = copy image), so a hand-rolled classifier reads cleaner than a hybrid. `TapCountClassifier` is intentionally pure — no clocks, no `Handler`, no Android imports. The Fragment supplies `now` (uptime millis) and decides when to call `resolve()`. That makes the classifier unit-testable in plain JUnit (see `TapCountClassifierTest`). The touch listener filters out scrolls before counting taps: an `ACTION_DOWN`/`ACTION_UP` pair only feeds the classifier if the finger moved less than the system touch slop. Returning `false` from the listener avoids consuming the event, so the ScrollView keeps its scroll behavior on tall comics. The classifier's three outputs are routed through `handleClassification`, which is wired to no-ops for now — subsequent commits replace the no-ops with `loadRandomComic`, `copyUrlToClipboard`, and `copyImageToClipboard` respectively. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small OkHttp-based client and wires it into the Fragment's single-tap path. The whole network surface fits in one file: XkcdApiClient.fetchLatest() / fetchByNumber(num) / openImageStream() fetchRandom() picks a number in [1, latestNum] and keeps picking until one returns a real comic. The only way it returns null is if the initial "latest comic" probe fails (network down). xkcd #404 is the joke "page not found" comic that returns HTTP 404 on its JSON endpoint, so we loop past it rather than bound retries — the loop converges in 1–2 picks on a healthy network and never gives up while the network works. Defensive parsing in parseComic() rejects any image URL that isn't `https://`, so a future MITM that swaps in `http://` doesn't break the plugin's HTTPS-only claim. Fragment wiring: - `loadRandomComic()` runs the fetch + decode on Dispatchers.IO and shows the result via lifecycleScope's Main dispatcher. - Rapid single-taps no-op while a previous fetch is in flight. - Read is bounded to 5 MB with cooperative cancellation so the coroutine respects lifecycleScope teardown mid-download. - Plain `BitmapFactory.decodeByteArray(...)` for now; for very large images on low-end devices, see Android's bounded-bitmap-decoding pattern at https://developer.android.com/topic/performance/graphics/load-bitmap Adds OkHttp 4.12.0 as the only new build dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires DOUBLE and TRIPLE tap classifications to clipboard handlers and
buffers the last comic's PNG bytes in memory so triple-tap can copy
the image without re-downloading or re-encoding the rendered Bitmap.
Text — the easy path: `ClipboardManager.setPrimaryClip(ClipData.newPlainText(...))`
needs no permission. Android doesn't gate clipboard with a
`<uses-permission>` either; foreground apps (which a plugin always is)
can write freely.
Image — the FileProvider hop:
1. Plugin `<provider>` declarations in the manifest are dead code —
plugins are DexClassLoader-loaded, not installed as Android apps,
so PackageManager never registers them. The escape valve is to
route through the host IDE's existing FileProvider authority.
2. The host's file_provider_paths.xml exposes filesDir, so we write
the buffered PNG to ctx.filesDir/xkcd_share/last.png and ask
FileProvider.getUriForFile(ctx, "${ctx.packageName}.providers.fileprovider",
target) for a content URI.
3. `ClipData.newUri` queries the ContentResolver for the URI's MIME
type — .png resolves to image/png, so paste targets see image/*
and offer to paste the image, not the URL string.
Toasts on every action so the user can tell the copy happened — the
system clipboard is invisible without feedback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opts the plugin class into `DocumentationExtension` and registers
CoGo's per-plugin help API:
Tier 1 (summary) — one-liner shown when long-pressing the tab
Tier 2 (detail) — HTML paragraph revealed by "See More"
Tier 3 (button) — full HTML walkthrough page served at
http://localhost:6174/plugin/<pluginId>/<uri>
`getTier3DocsAssetPath() = "docs"` tells the host to walk
`src/main/assets/docs/` at install time and serve each file (CSS +
HTML) at the URL above; the asset bundle's own files reference each
other with relative paths.
The walkthrough page itself is the canonical "how to write a CoGo
plugin" example for this codebase — a 7-step tour aligned with the
commit history.
Plugin Manager icons added too: day + night variants under
src/main/assets/, declared via `plugin.icon_day` / `plugin.icon_night`
meta-data. Required for debug-built `.cgp`s; release builds skip the
check. Includes `plugin.description` for the Plugin Manager listing.
Includes xkcd attribution. xkcd comics are © Randall Munroe and
licensed CC BY-NC 2.5 (https://xkcd.com/license.html). The Tier-3
page surfaces this; the plugin's in-panel UI already shows a
visible attribution line per the previous commit's strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A light source-tree README aimed at developers reading the plugin in GitHub or in their editor. The primary tutorial lives in the Tier-3 docs page (src/main/assets/docs/index.html), discoverable inside CoGo via long-press → "See More" → "Code walkthrough" on the XKCD tab. The README points readers there and surfaces: - what the plugin does in two sentences - build + test commands - the source-layout map - the xkcd attribution / CC BY-NC 2.5 license note Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ions Applies the findings from the pre-push code review. Bugfixes: - `XkcdApiClient.parseComic` — host allowlist instead of just scheme. Previously accepted any `https://...` URL from the JSON's `img` field; now restricts to `https://imgs.xkcd.com/...`. Protects against a malicious / MITM response pointing the bitmap decoder at an attacker-controlled HTTPS server. - `XkcdApiClient.fetchRandom` — made suspend + added `coroutineContext.ensureActive()` at the top of each loop iteration. Before, an unbounded loop with no cancellation point would run to completion if the Fragment was torn down mid-fetch on a flaky network. Now it exits with `CancellationException` cooperatively. - `XkcdApiClient.getJson` — bound the JSON read at 64 KB via `Content-Length` check. Stops a pathological response from OOM-ing the parser; xkcd's JSON is < 1 KB in practice so the cap is generous. Conventions parity with other plugin-examples plugins: - `minSdk` dropped from 28 → 26 to match apk-viewer / keystore-generator / markdown-preview / snippets / ndk-installer-plugin. No API-28-only call sites in this code; the stricter target was incidental. - Dead `R.string.tab_title` removed from `strings.xml` — `XkcdRandomPlugin.getEditorTabs()` hardcodes `title = "XKCD"` so the string was never read. Documentation honesty: - `XkcdApiClient` class doc + per-method docs telegraph that all public methods do blocking HTTP I/O and must be called from `Dispatchers.IO`. `fetchRandom` is suspend; the others are plain blocking funs. - `XkcdPanelFragment.lastBytes` field doc spells out the 5 MB heap-pin tradeoff and lists the alternatives (re-fetch, re-compress, disk cache) so a copy-paster doesn't internalize the pattern as default. - `Random.nextInt(1, latest.num + 1)` annotated with `// upper bound exclusive` next to the call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in TabItem.tooltipTag from the paired CoGo PR (appdevforall/CodeOnTheGo#1297) so random-xkcd can wire its bottom-sheet tab to its own tooltip entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Set `tooltipTag = TOOLTIP_TAG_TAB` on the bottom-sheet TabItem.
Long-pressing the XKCD tab now surfaces the plugin's own Tier-1
tooltip ("Random xkcd comic. Tap to roll a new one.") instead of
the generic platform placeholder. Requires the paired CoGo PR
(appdevforall/CodeOnTheGo#1297).
- Tutorial (assets/docs/index.html):
- Section 3 (bottom-sheet tab UI): include `tooltipTag` in the
TabItem code example + one paragraph explaining the wire to
Step 7's DocumentationExtension.
- Section 7 (tooltip): back-reference noting that `tag` here is
the same string as `TabItem.tooltipTag`.
- Tightening pass: -16% words (2482 → 2087). Cuts: end-to-end
recap section (duplicate of intro callout), sandbox-model
section collapsed to a 3-bullet summary, xkcd license section
halved, redundant phrasing across step intros, dropped the
standalone "6c. User feedback" subsection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,353 @@ | |||
| package com.codeonthego.xkcdrandom.fragments | |||
There was a problem hiding this comment.
for consistency with the existing plugin examples, please use org.appdevforall. as the package name
|
|
||
| <meta-data | ||
| android:name="plugin.author" | ||
| android:value="Code on the Go Team" /> |
There was a problem hiding this comment.
Please use App Dev For All as plugin author
|
I wonder if having the controls just like it is on the XKCD website is better UX, what do you think @fryanpan
|
Yes, that would be nicer UX. I can make that edit! Wasn't happy that we had to add a custom tap handler either...makes the demo more complicated than it needs to be. With buttons on screen for going to random, next, prev,etc. we only need to do single tap (copy url) and double tap (copy image) and I can get rid of the custom tap recognizer. That will be nicer too. |


Description
Adds the
random-xkcdplugin as a small plugin example that demonstrates a few concepts for building plugins.FileProviderauthorityassets/docs/Can review as a whole PR, or follow the commits that go through the same steps as the tutorial doc.
Recording from Claude going through all flows using mobile-mcp (and I also tested manually)
xkcd_qa.mp4
Ticket
ADFA-2433
Observation
xkcd CC BY-NC 2.5 attribution is surfaced in the panel (always-visible credit line beneath every comic) and in the Tier-3 docs.
Had to make a corresponding minor change in CodeOnTheGo repo. See matching PR here: appdevforall/CodeOnTheGo#1297
Some open questions for the future
"XKCD". The current Plugin API /TabItemdoesn't provide a way to localize fromContext(all the other plugins also hardcode strings for now).Build + Test Status
./gradlew testDebugUnitTest— green (10/10 inTapCountClassifierTest)./gradlew assemblePlugin— green;random-xkcd.cgpbuilds at 5.0 MB (release)🤖 Generated with Claude Code